/*
 * Copyright (C) 2006 The Android Open Source Project
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package net.sqlcipher.database;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;

import net.sqlcipher.CrossProcessCursorWrapper;
import net.sqlcipher.Cursor;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.SQLException;
import net.sqlcipher.database.SQLiteDebug.DbStats;
import android.content.ContentValues;
import android.content.Context;
import android.os.Debug;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import android.util.Pair;

/**
 * Exposes methods to manage a SQLite database.
 * <p>
 * SQLiteDatabase has methods to create, delete, execute SQL commands, and perform other common
 * database management tasks.
 * <p>
 * See the Notepad sample application in the SDK for an example of creating and managing a database.
 * <p>
 * Database names must be unique within an application, not across all applications.
 * 
 * <h3>Localized Collation - ORDER BY</h3>
 * <p>
 * In addition to SQLite's default <code>BINARY</code> collator, Android supplies two more,
 * <code>LOCALIZED</code>, which changes with the system's current locale if you wire it up
 * correctly (XXX a link needed!), and <code>UNICODE</code>, which is the Unicode Collation
 * Algorithm and not tailored to the current locale.
 */
public class SQLiteDatabase extends SQLiteClosable {
    private static final String TAG = "Database";
    private static final int EVENT_DB_OPERATION = 52000;
    private static final int EVENT_DB_CORRUPT = 75004;

    public int status(int operation, boolean reset) {
        return native_status(operation, reset);
    }

    private static void loadICUData(Context context, File workingDir) {

        try {
            File icuDir = new File(workingDir, "icu");
            if (!icuDir.exists())
                icuDir.mkdirs();
            File icuDataFile = new File(icuDir, "icudt46l.dat");
            if (!icuDataFile.exists()) {
                ZipInputStream in = new ZipInputStream(context.getAssets().open("icudt46l.zip"));
                in.getNextEntry();
                OutputStream out = new FileOutputStream(icuDataFile);
                byte[] buf = new byte[1024];
                int len;
                while ((len = in.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }
                in.close();
                out.flush();
                out.close();
            }
        } catch (Exception e) {
            Log.e(TAG, "Error copying icu data file", e);
        }
    }

    public static void loadLibs(Context context) {
        loadLibs(context, context.getFilesDir());
    }

    public static void loadLibs(Context context, File workingDir) {
        System.loadLibrary("stlport_shared");
        System.loadLibrary("sqlcipher_android");
        System.loadLibrary("database_sqlcipher");

        boolean systemICUFileExists = new File("/system/usr/icu/icudt46l.dat").exists();

        String icuRootPath = systemICUFileExists ? "/system/usr" : workingDir.getAbsolutePath();
        setICURoot(icuRootPath);
        if (!systemICUFileExists) {
            loadICUData(context, workingDir);
        }
    }

    /**
     * Algorithms used in ON CONFLICT clause http://www.sqlite.org/lang_conflict.html
     */
    /**
     * When a constraint violation occurs, an immediate ROLLBACK occurs, thus ending the current
     * transaction, and the command aborts with a return code of SQLITE_CONSTRAINT. If no
     * transaction is active (other than the implied transaction that is created on every command)
     * then this algorithm works the same as ABORT.
     */
    public static final int CONFLICT_ROLLBACK = 1;

    /**
     * When a constraint violation occurs,no ROLLBACK is executed so changes from prior commands
     * within the same transaction are preserved. This is the default behavior.
     */
    public static final int CONFLICT_ABORT = 2;

    /**
     * When a constraint violation occurs, the command aborts with a return code SQLITE_CONSTRAINT.
     * But any changes to the database that the command made prior to encountering the constraint
     * violation are preserved and are not backed out.
     */
    public static final int CONFLICT_FAIL = 3;

    /**
     * When a constraint violation occurs, the one row that contains the constraint violation is not
     * inserted or changed. But the command continues executing normally. Other rows before and
     * after the row that contained the constraint violation continue to be inserted or updated
     * normally. No error is returned.
     */
    public static final int CONFLICT_IGNORE = 4;

    /**
     * When a UNIQUE constraint violation occurs, the pre-existing rows that are causing the
     * constraint violation are removed prior to inserting or updating the current row. Thus the
     * insert or update always occurs. The command continues executing normally. No error is
     * returned. If a NOT NULL constraint violation occurs, the NULL value is replaced by the
     * default value for that column. If the column has no default value, then the ABORT algorithm
     * is used. If a CHECK constraint violation occurs then the IGNORE algorithm is used. When this
     * conflict resolution strategy deletes rows in order to satisfy a constraint, it does not
     * invoke delete triggers on those rows. This behavior might change in a future release.
     */
    public static final int CONFLICT_REPLACE = 5;

    /**
     * use the following when no conflict action is specified.
     */
    public static final int CONFLICT_NONE = 0;
    private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ",
            " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};

    /**
     * Maximum Length Of A LIKE Or GLOB Pattern The pattern matching algorithm used in the default
     * LIKE and GLOB implementation of SQLite can exhibit O(N^2) performance (where N is the number
     * of characters in the pattern) for certain pathological cases. To avoid denial-of-service
     * attacks the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH
     * bytes. The default value of this limit is 50000. A modern workstation can evaluate even a
     * pathological LIKE or GLOB pattern of 50000 bytes relatively quickly. The denial of service
     * problem only comes into play when the pattern length gets into millions of bytes.
     * Nevertheless, since most useful LIKE or GLOB patterns are at most a few dozen bytes in
     * length, paranoid application developers may want to reduce this parameter to something in the
     * range of a few hundred if they know that external users are able to generate arbitrary
     * patterns.
     */
    public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000;

    /**
     * Flag for {@link #openDatabase} to open the database for reading and writing. If the disk is
     * full, this may fail even before you actually write anything.
     * 
     * {@more} Note that the value of this flag is 0, so it is the default.
     */
    public static final int OPEN_READWRITE = 0x00000000; // update native code if changing

    /**
     * Flag for {@link #openDatabase} to open the database for reading only. This is the only
     * reliable way to open a database if the disk may be full.
     */
    public static final int OPEN_READONLY = 0x00000001; // update native code if changing

    private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing

    /**
     * Flag for {@link #openDatabase} to open the database without support for localized collators.
     * 
     * {@more} This causes the collator <code>LOCALIZED</code> not to be created. You must be
     * consistent when using this flag to use the setting the database was created with. If this is
     * set, {@link #setLocale} will do nothing.
     */
    public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing

    /**
     * Flag for {@link #openDatabase} to create the database file if it does not already exist.
     */
    public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing

    /**
     * Indicates whether the most-recently started transaction has been marked as successful.
     */
    private boolean mInnerTransactionIsSuccessful;

    /**
     * Valid during the life of a transaction, and indicates whether the entire transaction (the
     * outer one and all of the inner ones) so far has been successful.
     */
    private boolean mTransactionIsSuccessful;

    /**
     * Valid during the life of a transaction.
     */
    private SQLiteTransactionListener mTransactionListener;

    /** Synchronize on this when accessing the database */
    private final ReentrantLock mLock = new ReentrantLock(true);

    private long mLockAcquiredWallTime = 0L;
    private long mLockAcquiredThreadTime = 0L;

    // limit the frequency of complaints about each database to one within 20 sec
    // unless run command adb shell setprop log.tag.Database VERBOSE
    private static final int LOCK_WARNING_WINDOW_IN_MS = 20000;
    /** If the lock is held this long then a warning will be printed when it is released. */
    private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS = 300;
    private static final int LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS = 100;
    private static final int LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT = 2000;

    private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000;

    // The pattern we remove from database filenames before
    // potentially logging them.
    private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");

    private long mLastLockMessageTime = 0L;

    // Things related to query logging/sampling for debugging
    // slow/frequent queries during development. Always log queries
    // which take (by default) 500ms+; shorter queries are sampled
    // accordingly. Commit statements, which are typically slow, are
    // logged together with the most recently executed SQL statement,
    // for disambiguation. The 500ms value is configurable via a
    // SystemProperty, but developers actively debugging database I/O
    // should probably use the regular log tunable,
    // LOG_SLOW_QUERIES_PROPERTY, defined below.
    private static int sQueryLogTimeInMillis = 0; // lazily initialized
    private static final int QUERY_LOG_SQL_LENGTH = 64;
    private static final String COMMIT_SQL = "COMMIT;";
    private final Random mRandom = new Random();
    private String mLastSqlStatement = null;

    // String prefix for slow database query EventLog records that show
    // lock acquistions of the database.
    /* package */static final String GET_LOCK_LOG_PREFIX = "GETLOCK:";

    /** Used by native code, do not rename */
    /* package */int mNativeHandle = 0;

    /** Used to make temp table names unique */
    /* package */int mTempTableSequence = 0;

    /** The path for the database file */
    private String mPath;

    /** The anonymized path for the database file for logging purposes */
    private String mPathForLogs = null; // lazily populated

    /** The flags passed to open/create */
    private int mFlags;

    /** The optional factory to use when creating new Cursors */
    private CursorFactory mFactory;

    private WeakHashMap<SQLiteClosable, Object> mPrograms;

    /**
     * for each instance of this class, a cache is maintained to store the compiled query statement
     * ids returned by sqlite database. key = sql statement with "?" for bind args value =
     * {@link SQLiteCompiledSql} If an application opens the database and keeps it open during its
     * entire life, then there will not be an overhead of compilation of sql statements by sqlite.
     * 
     * why is this cache NOT static? because sqlite attaches compiledsql statements to the struct
     * created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is invoked.
     * 
     * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method (@link
     * setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because most of the
     * apps don't use "?" syntax in their sql, caching is not useful for them.
     */
    /* package */Map<String, SQLiteCompiledSql> mCompiledQueries = new HashMap<String, SQLiteCompiledSql>();
    /**
     * @hide
     */
    public static final int MAX_SQL_CACHE_SIZE = 250;
    private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance
    private int mCacheFullWarnings;
    private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1;

    /** maintain stats about number of cache hits and misses */
    private int mNumCacheHits;
    private int mNumCacheMisses;

    /** the following 2 members maintain the time when a database is opened and closed */
    private String mTimeOpened = null;
    private String mTimeClosed = null;

    /** Used to find out where this object was created in case it never got closed. */
    private Throwable mStackTrace = null;

    // System property that enables logging of slow queries. Specify the threshold in ms.
    private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold";
    private final int mSlowQueryThreshold;

    /**
     * @param closable
     */
    void addSQLiteClosable(SQLiteClosable closable) {
        lock();
        try {
            mPrograms.put(closable, null);
        } finally {
            unlock();
        }
    }

    void removeSQLiteClosable(SQLiteClosable closable) {
        lock();
        try {
            mPrograms.remove(closable);
        } finally {
            unlock();
        }
    }

    @Override
    protected void onAllReferencesReleased() {
        if (isOpen()) {
            if (SQLiteDebug.DEBUG_SQL_CACHE) {
                mTimeClosed = getTime();
            }
            dbclose();
        }
    }

    /**
     * Attempts to release memory that SQLite holds but does not require to operate properly.
     * Typically this memory will come from the page cache.
     * 
     * @return the number of bytes actually released
     */
    static public native int releaseMemory();

    /**
     * Control whether or not the SQLiteDatabase is made thread-safe by using locks around critical
     * sections. This is pretty expensive, so if you know that your DB will only be used by a single
     * thread then you should set this to false. The default is true.
     * 
     * @param lockingEnabled set to true to enable locks, false otherwise
     */
    public void setLockingEnabled(boolean lockingEnabled) {
        mLockingEnabled = lockingEnabled;
    }

    /**
     * If set then the SQLiteDatabase is made thread-safe by using locks around critical sections
     */
    private boolean mLockingEnabled = true;

    /* package */void onCorruption() {
        Log.e(TAG, "Removing corrupt database: " + mPath);
        // EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
        try {
            // Close the database (if we can), which will cause subsequent operations to fail.
            close();
        } finally {
            // Delete the corrupt file. Don't re-create it now -- that would just confuse people
            // -- but the next time someone tries to open it, they can set it up from scratch.
            if (!mPath.equalsIgnoreCase(":memory")) {
                // delete is only for non-memory database files
                new File(mPath).delete();
            }
        }
    }

    /**
     * Locks the database for exclusive access. The database lock must be held when touch the native
     * sqlite3* object since it is single threaded and uses a polling lock contention algorithm. The
     * lock is recursive, and may be acquired multiple times by the same thread. This is a no-op if
     * mLockingEnabled is false.
     * 
     * @see #unlock()
     */
    /* package */void lock() {
        if (!mLockingEnabled)
            return;
        mLock.lock();
        if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
            if (mLock.getHoldCount() == 1) {
                // Use elapsed real-time since the CPU may sleep when waiting for IO
                mLockAcquiredWallTime = SystemClock.elapsedRealtime();
                mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
            }
        }
    }

    /**
     * Locks the database for exclusive access. The database lock must be held when touch the native
     * sqlite3* object since it is single threaded and uses a polling lock contention algorithm. The
     * lock is recursive, and may be acquired multiple times by the same thread.
     * 
     * @see #unlockForced()
     */
    private void lockForced() {
        mLock.lock();
        if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
            if (mLock.getHoldCount() == 1) {
                // Use elapsed real-time since the CPU may sleep when waiting for IO
                mLockAcquiredWallTime = SystemClock.elapsedRealtime();
                mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
            }
        }
    }

    /**
     * Releases the database lock. This is a no-op if mLockingEnabled is false.
     * 
     * @see #unlock()
     */
    /* package */void unlock() {
        if (!mLockingEnabled)
            return;
        if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
            if (mLock.getHoldCount() == 1) {
                checkLockHoldTime();
            }
        }
        mLock.unlock();
    }

    /**
     * Releases the database lock.
     * 
     * @see #unlockForced()
     */
    private void unlockForced() {
        if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING) {
            if (mLock.getHoldCount() == 1) {
                checkLockHoldTime();
            }
        }
        mLock.unlock();
    }

    private void checkLockHoldTime() {
        // Use elapsed real-time since the CPU may sleep when waiting for IO
        long elapsedTime = SystemClock.elapsedRealtime();
        long lockedTime = elapsedTime - mLockAcquiredWallTime;
        if (lockedTime < LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT
                && !Log.isLoggable(TAG, Log.VERBOSE)
                && (elapsedTime - mLastLockMessageTime) < LOCK_WARNING_WINDOW_IN_MS) {
            return;
        }
        if (lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS) {
            int threadTime =
                    (int) ((Debug.threadCpuTimeNanos() - mLockAcquiredThreadTime) / 1000000);
            if (threadTime > LOCK_ACQUIRED_WARNING_THREAD_TIME_IN_MS
                    || lockedTime > LOCK_ACQUIRED_WARNING_TIME_IN_MS_ALWAYS_PRINT) {
                mLastLockMessageTime = elapsedTime;
                String msg =
                        "lock held on " + mPath + " for " + lockedTime + "ms. Thread time was "
                                + threadTime + "ms";
                if (SQLiteDebug.DEBUG_LOCK_TIME_TRACKING_STACK_TRACE) {
                    Log.d(TAG, msg, new Exception());
                } else {
                    Log.d(TAG, msg);
                }
            }
        }
    }

    /**
     * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
     * the work done in that transaction and all of the nested transactions will be committed or
     * rolled back. The changes will be rolled back if any transaction is ended without being marked
     * as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
     * 
     * <p>
     * Here is the standard idiom for transactions:
     * 
     * <pre>
     *   db.beginTransaction();
     *   try {
     *     ...
     *     db.setTransactionSuccessful();
     *   } finally {
     *     db.endTransaction();
     *   }
     * </pre>
     */
    public void beginTransaction() {
        beginTransactionWithListener(null /* transactionStatusCallback */);
    }

    /**
     * Begins a transaction. Transactions can be nested. When the outer transaction is ended all of
     * the work done in that transaction and all of the nested transactions will be committed or
     * rolled back. The changes will be rolled back if any transaction is ended without being marked
     * as clean (by calling setTransactionSuccessful). Otherwise they will be committed.
     * 
     * <p>
     * Here is the standard idiom for transactions:
     * 
     * <pre>
     *   db.beginTransactionWithListener(listener);
     *   try {
     *     ...
     *     db.setTransactionSuccessful();
     *   } finally {
     *     db.endTransaction();
     *   }
     * </pre>
     * 
     * @param transactionListener listener that should be notified when the transaction begins,
     *        commits, or is rolled back, either explicitly or by a call to
     *        {@link #yieldIfContendedSafely}.
     */
    public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
        lockForced();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        boolean ok = false;
        try {
            // If this thread already had the lock then get out
            if (mLock.getHoldCount() > 1) {
                if (mInnerTransactionIsSuccessful) {
                    String msg =
                            "Cannot call beginTransaction between "
                                    + "calling setTransactionSuccessful and endTransaction";
                    IllegalStateException e = new IllegalStateException(msg);
                    Log.e(TAG, "beginTransaction() failed", e);
                    throw e;
                }
                ok = true;
                return;
            }

            // This thread didn't already have the lock, so begin a database
            // transaction now.
            execSQL("BEGIN EXCLUSIVE;");
            mTransactionListener = transactionListener;
            mTransactionIsSuccessful = true;
            mInnerTransactionIsSuccessful = false;
            if (transactionListener != null) {
                try {
                    transactionListener.onBegin();
                } catch (RuntimeException e) {
                    execSQL("ROLLBACK;");
                    throw e;
                }
            }
            ok = true;
        } finally {
            if (!ok) {
                // beginTransaction is called before the try block so we must release the lock in
                // the case of failure.
                unlockForced();
            }
        }
    }

    /**
     * End a transaction. See beginTransaction for notes about how to use this and when transactions
     * are committed and rolled back.
     */
    public void endTransaction() {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        if (!mLock.isHeldByCurrentThread()) {
            throw new IllegalStateException("no transaction pending");
        }
        try {
            if (mInnerTransactionIsSuccessful) {
                mInnerTransactionIsSuccessful = false;
            } else {
                mTransactionIsSuccessful = false;
            }
            if (mLock.getHoldCount() != 1) {
                return;
            }
            RuntimeException savedException = null;
            if (mTransactionListener != null) {
                try {
                    if (mTransactionIsSuccessful) {
                        mTransactionListener.onCommit();
                    } else {
                        mTransactionListener.onRollback();
                    }
                } catch (RuntimeException e) {
                    savedException = e;
                    mTransactionIsSuccessful = false;
                }
            }
            if (mTransactionIsSuccessful) {
                execSQL(COMMIT_SQL);
            } else {
                try {
                    execSQL("ROLLBACK;");
                    if (savedException != null) {
                        throw savedException;
                    }
                } catch (SQLException e) {
                    if (Config.LOGD) {
                        Log.d(TAG, "exception during rollback, maybe the DB previously "
                                + "performed an auto-rollback");
                    }
                }
            }
        } finally {
            mTransactionListener = null;
            unlockForced();
            if (Config.LOGV) {
                Log.v(TAG,
                        "unlocked " + Thread.currentThread() + ", holdCount is "
                                + mLock.getHoldCount());
            }
        }
    }

    /**
     * Marks the current transaction as successful. Do not do any more database work between calling
     * this and calling endTransaction. Do as little non-database work as possible in that situation
     * too. If any errors are encountered between this and endTransaction the transaction will still
     * be committed.
     * 
     * @throws IllegalStateException if the current thread is not in a transaction or the
     *         transaction is already marked as successful.
     */
    public void setTransactionSuccessful() {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        if (!mLock.isHeldByCurrentThread()) {
            throw new IllegalStateException("no transaction pending");
        }
        if (mInnerTransactionIsSuccessful) {
            throw new IllegalStateException(
                    "setTransactionSuccessful may only be called once per call to beginTransaction");
        }
        mInnerTransactionIsSuccessful = true;
    }

    /**
     * return true if there is a transaction pending
     */
    public boolean inTransaction() {
        return mLock.getHoldCount() > 0;
    }

    /**
     * Checks if the database lock is held by this thread.
     * 
     * @return true, if this thread is holding the database lock.
     */
    public boolean isDbLockedByCurrentThread() {
        return mLock.isHeldByCurrentThread();
    }

    /**
     * Checks if the database is locked by another thread. This is just an estimate, since this
     * status can change at any time, including after the call is made but before the result has
     * been acted upon.
     * 
     * @return true, if the database is locked by another thread
     */
    public boolean isDbLockedByOtherThreads() {
        return !mLock.isHeldByCurrentThread() && mLock.isLocked();
    }

    /**
     * Temporarily end the transaction to let other threads run. The transaction is assumed to be
     * successful so far. Do not call setTransactionSuccessful before calling this. When this
     * returns a new transaction will have been created but not marked as successful.
     * 
     * @return true if the transaction was yielded
     * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock
     *             will not be yielded. Use yieldIfContendedSafely instead.
     */
    @Deprecated
    public boolean yieldIfContended() {
        return yieldIfContendedHelper(false /* do not check yielding */, -1 /* sleepAfterYieldDelay */);
    }

    /**
     * Temporarily end the transaction to let other threads run. The transaction is assumed to be
     * successful so far. Do not call setTransactionSuccessful before calling this. When this
     * returns a new transaction will have been created but not marked as successful. This assumes
     * that there are no nested transactions (beginTransaction has only been called once) and will
     * throw an exception if that is not the case.
     * 
     * @return true if the transaction was yielded
     */
    public boolean yieldIfContendedSafely() {
        return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay */);
    }

    /**
     * Temporarily end the transaction to let other threads run. The transaction is assumed to be
     * successful so far. Do not call setTransactionSuccessful before calling this. When this
     * returns a new transaction will have been created but not marked as successful. This assumes
     * that there are no nested transactions (beginTransaction has only been called once) and will
     * throw an exception if that is not the case.
     * 
     * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if the
     *        lock was actually yielded. This will allow other background threads to make some more
     *        progress than they would if we started the transaction immediately.
     * @return true if the transaction was yielded
     */
    public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
        return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay);
    }

    private boolean yieldIfContendedHelper(boolean checkFullyYielded, long sleepAfterYieldDelay) {
        if (mLock.getQueueLength() == 0) {
            // Reset the lock acquire time since we know that the thread was willing to yield
            // the lock at this time.
            mLockAcquiredWallTime = SystemClock.elapsedRealtime();
            mLockAcquiredThreadTime = Debug.threadCpuTimeNanos();
            return false;
        }
        setTransactionSuccessful();
        SQLiteTransactionListener transactionListener = mTransactionListener;
        endTransaction();
        if (checkFullyYielded) {
            if (this.isDbLockedByCurrentThread()) {
                throw new IllegalStateException(
                        "Db locked more than once. yielfIfContended cannot yield");
            }
        }
        if (sleepAfterYieldDelay > 0) {
            // Sleep for up to sleepAfterYieldDelay milliseconds, waking up periodically to
            // check if anyone is using the database. If the database is not contended,
            // retake the lock and return.
            long remainingDelay = sleepAfterYieldDelay;
            while (remainingDelay > 0) {
                try {
                    Thread.sleep(remainingDelay < SLEEP_AFTER_YIELD_QUANTUM ? remainingDelay
                            : SLEEP_AFTER_YIELD_QUANTUM);
                } catch (InterruptedException e) {
                    Thread.interrupted();
                }
                remainingDelay -= SLEEP_AFTER_YIELD_QUANTUM;
                if (mLock.getQueueLength() == 0) {
                    break;
                }
            }
        }
        beginTransactionWithListener(transactionListener);
        return true;
    }

    /**
     * Maps table names to info about what to which _sync_time column to set to NULL on an update.
     * This is used to support syncing.
     */
    private final Map<String, SyncUpdateInfo> mSyncUpdateInfo =
            new HashMap<String, SyncUpdateInfo>();

    public Map<String, String> getSyncedTables() {
        synchronized (mSyncUpdateInfo) {
            HashMap<String, String> tables = new HashMap<String, String>();
            for (String table : mSyncUpdateInfo.keySet()) {
                SyncUpdateInfo info = mSyncUpdateInfo.get(table);
                if (info.deletedTable != null) {
                    tables.put(table, info.deletedTable);
                }
            }
            return tables;
        }
    }

    /**
     * Internal class used to keep track what needs to be marked as changed when an update occurs.
     * This is used for syncing, so the sync engine knows what data has been updated locally.
     */
    static private class SyncUpdateInfo {
        /**
         * Creates the SyncUpdateInfo class.
         * 
         * @param masterTable The table to set _sync_time to NULL in
         * @param deletedTable The deleted table that corresponds to the master table
         * @param foreignKey The key that refers to the primary key in table
         */
        SyncUpdateInfo(String masterTable, String deletedTable, String foreignKey) {
            this.masterTable = masterTable;
            this.deletedTable = deletedTable;
            this.foreignKey = foreignKey;
        }

        /** The table containing the _sync_time column */
        String masterTable;

        /** The deleted table that corresponds to the master table */
        String deletedTable;

        /**
         * The key in the local table the row in table. It may be _id, if table is the local table.
         */
        String foreignKey;
    }

    /**
     * Used to allow returning sub-classes of {@link Cursor} when calling query.
     */
    public interface CursorFactory {
        /**
         * See
         * {@link SQLiteCursor#SQLiteCursor(SQLiteDatabase, SQLiteCursorDriver, String, SQLiteQuery)}
         * .
         */
        public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
                String editTable, SQLiteQuery query);
    }

    /**
     * Open the database according to the flags {@link #OPEN_READWRITE} {@link #OPEN_READONLY}
     * {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
     * 
     * <p>
     * Sets the locale of the database to the the system's current locale. Call {@link #setLocale}
     * if you would like something else.
     * </p>
     * 
     * @param path to database file to open and/or create
     * @param factory an optional factory class that is called to instantiate a cursor when query is
     *        called, or null for default
     * @param flags to control database access mode
     * @return the newly opened database
     * @throws SQLiteException if the database cannot be opened
     */
    public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory,
            int flags, SQLiteDatabaseHook databaseHook) {
        SQLiteDatabase sqliteDatabase = null;
        try {
            // Open the database.
            sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, databaseHook);
            if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
                sqliteDatabase.enableSqlTracing(path);
            }
            if (SQLiteDebug.DEBUG_SQL_TIME) {
                sqliteDatabase.enableSqlProfiling(path);
            }
        } catch (SQLiteDatabaseCorruptException e) {
            // Try to recover from this, if we can.
            // TODO: should we do this for other open failures?
            Log.e(TAG, "Deleting and re-creating corrupt database " + path, e);
            // EventLog.writeEvent(EVENT_DB_CORRUPT, path);
            if (!path.equalsIgnoreCase(":memory")) {
                // delete is only for non-memory database files
                new File(path).delete();
            }
            sqliteDatabase = new SQLiteDatabase(path, password, factory, flags, databaseHook);
        }
        ActiveDatabases.getInstance().mActiveDatabases.add(new WeakReference<SQLiteDatabase>(
                sqliteDatabase));
        return sqliteDatabase;
    }

    public static SQLiteDatabase openOrCreateDatabase(File file, String password,
            CursorFactory factory, SQLiteDatabaseHook databaseHook) {
        return openOrCreateDatabase(file.getPath(), password, factory, databaseHook);
    }

    public static SQLiteDatabase openOrCreateDatabase(String path, String password,
            CursorFactory factory, SQLiteDatabaseHook databaseHook) {
        return openDatabase(path, password, factory, CREATE_IF_NECESSARY, databaseHook);
    }

    /**
     * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY).
     */
    public static SQLiteDatabase openOrCreateDatabase(File file, String password,
            CursorFactory factory) {
        return openOrCreateDatabase(file.getPath(), password, factory, null);
    }

    /**
     * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY).
     */

    public static SQLiteDatabase openOrCreateDatabase(String path, String password,
            CursorFactory factory) {
        return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null);
    }

    public static SQLiteDatabase openDatabase(String path, String password, CursorFactory factory,
            int flags) {
        return openDatabase(path, password, factory, CREATE_IF_NECESSARY, null);
    }

    /**
     * Create a memory backed SQLite database. Its contents will be destroyed when the database is
     * closed.
     * 
     * <p>
     * Sets the locale of the database to the the system's current locale. Call {@link #setLocale}
     * if you would like something else.
     * </p>
     * 
     * @param factory an optional factory class that is called to instantiate a cursor when query is
     *        called
     * @return a SQLiteDatabase object, or null if the database can't be created
     */
    public static SQLiteDatabase create(CursorFactory factory, String password) {
        // This is a magic string with special meaning for SQLite.
        return openDatabase(":memory:", password, factory, CREATE_IF_NECESSARY);
    }

    /**
     * Close the database.
     */
    public void close() {

        if (!isOpen()) {
            return; // already closed
        }
        lock();
        try {
            closeClosable();
            // close this database instance - regardless of its reference count value
            onAllReferencesReleased();
        } finally {
            unlock();
        }
    }

    private void closeClosable() {
        /*
         * deallocate all compiled sql statement objects from mCompiledQueries cache. this should be
         * done before de-referencing all {@link SQLiteClosable} objects from this database object
         * because calling {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause
         * the database to be closed. sqlite doesn't let a database close if there are any
         * unfinalized statements - such as the compiled-sql objects in mCompiledQueries.
         */
        deallocCachedSqlStatements();

        Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<SQLiteClosable, Object> entry = iter.next();
            SQLiteClosable program = entry.getKey();
            if (program != null) {
                program.onAllReferencesReleasedFromContainer();
            }
        }
    }

    /**
     * Native call to close the database.
     */
    private native void dbclose();

    /**
     * Gets the database version.
     * 
     * @return the database version
     */
    public int getVersion() {
        SQLiteStatement prog = null;
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        try {
            prog = new SQLiteStatement(this, "PRAGMA user_version;");
            long version = prog.simpleQueryForLong();
            return (int) version;
        } finally {
            if (prog != null)
                prog.close();
            unlock();
        }
    }

    /**
     * Sets the database version.
     * 
     * @param version the new database version
     */
    public void setVersion(int version) {
        execSQL("PRAGMA user_version = " + version);
    }

    /**
     * Returns the maximum size the database may grow to.
     * 
     * @return the new maximum database size
     */
    public long getMaximumSize() {
        SQLiteStatement prog = null;
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        try {
            prog = new SQLiteStatement(this, "PRAGMA max_page_count;");
            long pageCount = prog.simpleQueryForLong();
            return pageCount * getPageSize();
        } finally {
            if (prog != null)
                prog.close();
            unlock();
        }
    }

    /**
     * Sets the maximum size the database will grow to. The maximum size cannot be set below the
     * current size.
     * 
     * @param numBytes the maximum database size, in bytes
     * @return the new maximum database size
     */
    public long setMaximumSize(long numBytes) {
        SQLiteStatement prog = null;
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        try {
            long pageSize = getPageSize();
            long numPages = numBytes / pageSize;
            // If numBytes isn't a multiple of pageSize, bump up a page
            if ((numBytes % pageSize) != 0) {
                numPages++;
            }
            prog = new SQLiteStatement(this, "PRAGMA max_page_count = " + numPages);
            long newPageCount = prog.simpleQueryForLong();
            return newPageCount * pageSize;
        } finally {
            if (prog != null)
                prog.close();
            unlock();
        }
    }

    /**
     * Returns the current database page size, in bytes.
     * 
     * @return the database page size, in bytes
     */
    public long getPageSize() {
        SQLiteStatement prog = null;
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        try {
            prog = new SQLiteStatement(this, "PRAGMA page_size;");
            long size = prog.simpleQueryForLong();
            return size;
        } finally {
            if (prog != null)
                prog.close();
            unlock();
        }
    }

    /**
     * Sets the database page size. The page size must be a power of two. This method does not work
     * if any data has been written to the database file, and must be called right after the
     * database has been created.
     * 
     * @param numBytes the database page size, in bytes
     */
    public void setPageSize(long numBytes) {
        execSQL("PRAGMA page_size = " + numBytes);
    }

    /**
     * Mark this table as syncable. When an update occurs in this table the _sync_dirty field will
     * be set to ensure proper syncing operation.
     * 
     * @param table the table to mark as syncable
     * @param deletedTable The deleted table that corresponds to the syncable table
     */
    public void markTableSyncable(String table, String deletedTable) {
        markTableSyncable(table, "_id", table, deletedTable);
    }

    /**
     * Mark this table as syncable, with the _sync_dirty residing in another table. When an update
     * occurs in this table the _sync_dirty field of the row in updateTable with the _id in
     * foreignKey will be set to ensure proper syncing operation.
     * 
     * @param table an update on this table will trigger a sync time removal
     * @param foreignKey this is the column in table whose value is an _id in updateTable
     * @param updateTable this is the table that will have its _sync_dirty
     */
    public void markTableSyncable(String table, String foreignKey, String updateTable) {
        markTableSyncable(table, foreignKey, updateTable, null);
    }

    /**
     * Mark this table as syncable, with the _sync_dirty residing in another table. When an update
     * occurs in this table the _sync_dirty field of the row in updateTable with the _id in
     * foreignKey will be set to ensure proper syncing operation.
     * 
     * @param table an update on this table will trigger a sync time removal
     * @param foreignKey this is the column in table whose value is an _id in updateTable
     * @param updateTable this is the table that will have its _sync_dirty
     * @param deletedTable The deleted table that corresponds to the updateTable
     */
    private void markTableSyncable(String table, String foreignKey, String updateTable,
            String deletedTable) {
        lock();
        try {
            native_execSQL("SELECT _sync_dirty FROM " + updateTable + " LIMIT 0");
            native_execSQL("SELECT " + foreignKey + " FROM " + table + " LIMIT 0");
        } finally {
            unlock();
        }

        SyncUpdateInfo info = new SyncUpdateInfo(updateTable, deletedTable, foreignKey);
        synchronized (mSyncUpdateInfo) {
            mSyncUpdateInfo.put(table, info);
        }
    }

    /**
     * Call for each row that is updated in a cursor.
     * 
     * @param table the table the row is in
     * @param rowId the row ID of the updated row
     */
    /* package */void rowUpdated(String table, long rowId) {
        SyncUpdateInfo info;
        synchronized (mSyncUpdateInfo) {
            info = mSyncUpdateInfo.get(table);
        }
        if (info != null) {
            execSQL("UPDATE " + info.masterTable + " SET _sync_dirty=1 WHERE _id=(SELECT "
                    + info.foreignKey + " FROM " + table + " WHERE _id=" + rowId + ")");
        }
    }

    /**
     * Finds the name of the first table, which is editable.
     * 
     * @param tables a list of tables
     * @return the first table listed
     */
    public static String findEditTable(String tables) {
        if (!TextUtils.isEmpty(tables)) {
            // find the first word terminated by either a space or a comma
            int spacepos = tables.indexOf(' ');
            int commapos = tables.indexOf(',');

            if (spacepos > 0 && (spacepos < commapos || commapos < 0)) {
                return tables.substring(0, spacepos);
            } else if (commapos > 0 && (commapos < spacepos || spacepos < 0)) {
                return tables.substring(0, commapos);
            }
            return tables;
        } else {
            throw new IllegalStateException("Invalid tables");
        }
    }

    /**
     * Compiles an SQL statement into a reusable pre-compiled statement object. The parameters are
     * identical to {@link #execSQL(String)}. You may put ?s in the statement and fill in those
     * values with {@link SQLiteProgram#bindString} and {@link SQLiteProgram#bindLong} each time you
     * want to run the statement. Statements may not return result sets larger than 1x1.
     * 
     * @param sql The raw SQL statement, may contain ? for unknown values to be bound later.
     * @return A pre-compiled {@link SQLiteStatement} object. Note that {@link SQLiteStatement}s are
     *         not synchronized, see the documentation for more details.
     */
    public SQLiteStatement compileStatement(String sql) throws SQLException {
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        try {
            return new SQLiteStatement(this, sql);
        } finally {
            unlock();
        }
    }

    /**
     * Query the given URL, returning a {@link Cursor} over the result set.
     * 
     * @param distinct true if you want each row to be unique, false otherwise.
     * @param table The table name to compile the query against.
     * @param columns A list of which columns to return. Passing null will return all columns, which
     *        is discouraged to prevent reading data from storage that isn't going to be used.
     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
     *        (excluding the WHERE itself). Passing null will return all rows for the given table.
     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
     *        from selectionArgs, in order that they appear in the selection. The values will be
     *        bound as Strings.
     * @param groupBy A filter declaring how to group rows, formatted as an SQL GROUP BY clause
     *        (excluding the GROUP BY itself). Passing null will cause the rows to not be grouped.
     * @param having A filter declare which row groups to include in the cursor, if row grouping is
     *        being used, formatted as an SQL HAVING clause (excluding the HAVING itself). Passing
     *        null will cause all row groups to be included, and is required when row grouping is
     *        not being used.
     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the
     *        ORDER BY itself). Passing null will use the default sort order, which may be
     *        unordered.
     * @param limit Limits the number of rows returned by the query, formatted as LIMIT clause.
     *        Passing null denotes no LIMIT clause.
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     * @see Cursor
     */
    public Cursor query(boolean distinct, String table, String[] columns, String selection,
            String[] selectionArgs, String groupBy, String having, String orderBy, String limit) {
        return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, groupBy,
                having, orderBy, limit);
    }

    /**
     * Query the given URL, returning a {@link Cursor} over the result set.
     * 
     * @param cursorFactory the cursor factory to use, or null for the default factory
     * @param distinct true if you want each row to be unique, false otherwise.
     * @param table The table name to compile the query against.
     * @param columns A list of which columns to return. Passing null will return all columns, which
     *        is discouraged to prevent reading data from storage that isn't going to be used.
     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
     *        (excluding the WHERE itself). Passing null will return all rows for the given table.
     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
     *        from selectionArgs, in order that they appear in the selection. The values will be
     *        bound as Strings.
     * @param groupBy A filter declaring how to group rows, formatted as an SQL GROUP BY clause
     *        (excluding the GROUP BY itself). Passing null will cause the rows to not be grouped.
     * @param having A filter declare which row groups to include in the cursor, if row grouping is
     *        being used, formatted as an SQL HAVING clause (excluding the HAVING itself). Passing
     *        null will cause all row groups to be included, and is required when row grouping is
     *        not being used.
     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the
     *        ORDER BY itself). Passing null will use the default sort order, which may be
     *        unordered.
     * @param limit Limits the number of rows returned by the query, formatted as LIMIT clause.
     *        Passing null denotes no LIMIT clause.
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     * @see Cursor
     */
    public Cursor queryWithFactory(CursorFactory cursorFactory, boolean distinct, String table,
            String[] columns, String selection, String[] selectionArgs, String groupBy,
            String having, String orderBy, String limit) {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        String sql =
                SQLiteQueryBuilder.buildQueryString(distinct, table, columns, selection, groupBy,
                        having, orderBy, limit);

        return rawQueryWithFactory(cursorFactory, sql, selectionArgs, findEditTable(table));
    }

    /**
     * Query the given table, returning a {@link Cursor} over the result set.
     * 
     * @param table The table name to compile the query against.
     * @param columns A list of which columns to return. Passing null will return all columns, which
     *        is discouraged to prevent reading data from storage that isn't going to be used.
     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
     *        (excluding the WHERE itself). Passing null will return all rows for the given table.
     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
     *        from selectionArgs, in order that they appear in the selection. The values will be
     *        bound as Strings.
     * @param groupBy A filter declaring how to group rows, formatted as an SQL GROUP BY clause
     *        (excluding the GROUP BY itself). Passing null will cause the rows to not be grouped.
     * @param having A filter declare which row groups to include in the cursor, if row grouping is
     *        being used, formatted as an SQL HAVING clause (excluding the HAVING itself). Passing
     *        null will cause all row groups to be included, and is required when row grouping is
     *        not being used.
     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the
     *        ORDER BY itself). Passing null will use the default sort order, which may be
     *        unordered.
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     * @see Cursor
     */
    public Cursor query(String table, String[] columns, String selection, String[] selectionArgs,
            String groupBy, String having, String orderBy) {

        return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy,
                null /* limit */);
    }

    /**
     * Query the given table, returning a {@link Cursor} over the result set.
     * 
     * @param table The table name to compile the query against.
     * @param columns A list of which columns to return. Passing null will return all columns, which
     *        is discouraged to prevent reading data from storage that isn't going to be used.
     * @param selection A filter declaring which rows to return, formatted as an SQL WHERE clause
     *        (excluding the WHERE itself). Passing null will return all rows for the given table.
     * @param selectionArgs You may include ?s in selection, which will be replaced by the values
     *        from selectionArgs, in order that they appear in the selection. The values will be
     *        bound as Strings.
     * @param groupBy A filter declaring how to group rows, formatted as an SQL GROUP BY clause
     *        (excluding the GROUP BY itself). Passing null will cause the rows to not be grouped.
     * @param having A filter declare which row groups to include in the cursor, if row grouping is
     *        being used, formatted as an SQL HAVING clause (excluding the HAVING itself). Passing
     *        null will cause all row groups to be included, and is required when row grouping is
     *        not being used.
     * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause (excluding the
     *        ORDER BY itself). Passing null will use the default sort order, which may be
     *        unordered.
     * @param limit Limits the number of rows returned by the query, formatted as LIMIT clause.
     *        Passing null denotes no LIMIT clause.
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     * @see Cursor
     */
    public Cursor query(String table, String[] columns, String selection, String[] selectionArgs,
            String groupBy, String having, String orderBy, String limit) {

        return query(false, table, columns, selection, selectionArgs, groupBy, having, orderBy,
                limit);
    }

    /**
     * Runs the provided SQL and returns a {@link Cursor} over the result set.
     * 
     * @param sql the SQL query. The SQL string must not be ; terminated
     * @param selectionArgs You may include ?s in where clause in the query, which will be replaced
     *        by the values from selectionArgs. The values will be bound as Strings.
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     */
    public Cursor rawQuery(String sql, String[] selectionArgs) {
        return rawQueryWithFactory(null, sql, selectionArgs, null);
    }

    /**
     * Runs the provided SQL and returns a cursor over the result set.
     * 
     * @param cursorFactory the cursor factory to use, or null for the default factory
     * @param sql the SQL query. The SQL string must not be ; terminated
     * @param selectionArgs You may include ?s in where clause in the query, which will be replaced
     *        by the values from selectionArgs. The values will be bound as Strings.
     * @param editTable the name of the first table, which is editable
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     */
    public Cursor rawQueryWithFactory(CursorFactory cursorFactory, String sql,
            String[] selectionArgs, String editTable) {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        long timeStart = 0;

        if (Config.LOGV || mSlowQueryThreshold != -1) {
            timeStart = System.currentTimeMillis();
        }

        SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);

        Cursor cursor = null;
        try {
            cursor = driver.query(cursorFactory != null ? cursorFactory : mFactory, selectionArgs);
        } finally {
            if (Config.LOGV || mSlowQueryThreshold != -1) {

                // Force query execution
                int count = -1;
                if (cursor != null) {
                    count = cursor.getCount();
                }

                long duration = System.currentTimeMillis() - timeStart;

                if (Config.LOGV || duration >= mSlowQueryThreshold) {
                    Log.v(TAG, "query ("
                            + duration
                            + " ms): "
                            + driver.toString()
                            + ", args are "
                            + (selectionArgs != null ? TextUtils.join(",", selectionArgs)
                                    : "<null>") + ", count is " + count);
                }
            }
        }
        return new CrossProcessCursorWrapper(cursor);
    }

    /**
     * Runs the provided SQL and returns a cursor over the result set. The cursor will read an
     * initial set of rows and the return to the caller. It will continue to read in batches and
     * send data changed notifications when the later batches are ready.
     * 
     * @param sql the SQL query. The SQL string must not be ; terminated
     * @param selectionArgs You may include ?s in where clause in the query, which will be replaced
     *        by the values from selectionArgs. The values will be bound as Strings.
     * @param initialRead set the initial count of items to read from the cursor
     * @param maxRead set the count of items to read on each iteration after the first
     * @return A {@link Cursor} object, which is positioned before the first entry. Note that
     *         {@link Cursor}s are not synchronized, see the documentation for more details.
     * 
     *         This work is incomplete and not fully tested or reviewed, so currently hidden.
     * @hide
     */
    public Cursor rawQuery(String sql, String[] selectionArgs, int initialRead, int maxRead) {
        SQLiteCursor c = (SQLiteCursor) rawQueryWithFactory(null, sql, selectionArgs, null);
        c.setLoadStyle(initialRead, maxRead);

        return c;
    }

    /**
     * Convenience method for inserting a row into the database.
     * 
     * @param table the table to insert the row into
     * @param nullColumnHack SQL doesn't allow inserting a completely empty row, so if initialValues
     *        is empty this column will explicitly be assigned a NULL value
     * @param values this map contains the initial column values for the row. The keys should be the
     *        column names and the values the column values
     * @return the row ID of the newly inserted row, or -1 if an error occurred
     */
    public long insert(String table, String nullColumnHack, ContentValues values) {
        try {
            return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
        } catch (SQLException e) {
            Log.e(TAG, "Error inserting " + values, e);
            return -1;
        }
    }

    /**
     * Convenience method for inserting a row into the database.
     * 
     * @param table the table to insert the row into
     * @param nullColumnHack SQL doesn't allow inserting a completely empty row, so if initialValues
     *        is empty this column will explicitly be assigned a NULL value
     * @param values this map contains the initial column values for the row. The keys should be the
     *        column names and the values the column values
     * @throws SQLException
     * @return the row ID of the newly inserted row, or -1 if an error occurred
     */
    public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
            throws SQLException {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    }

    /**
     * Convenience method for replacing a row in the database.
     * 
     * @param table the table in which to replace the row
     * @param nullColumnHack SQL doesn't allow inserting a completely empty row, so if initialValues
     *        is empty this row will explicitly be assigned a NULL value
     * @param initialValues this map contains the initial column values for the row. The key
     * @return the row ID of the newly inserted row, or -1 if an error occurred
     */
    public long replace(String table, String nullColumnHack, ContentValues initialValues) {
        try {
            return insertWithOnConflict(table, nullColumnHack, initialValues, CONFLICT_REPLACE);
        } catch (SQLException e) {
            Log.e(TAG, "Error inserting " + initialValues, e);
            return -1;
        }
    }

    /**
     * Convenience method for replacing a row in the database.
     * 
     * @param table the table in which to replace the row
     * @param nullColumnHack SQL doesn't allow inserting a completely empty row, so if initialValues
     *        is empty this row will explicitly be assigned a NULL value
     * @param initialValues this map contains the initial column values for the row. The key
     * @throws SQLException
     * @return the row ID of the newly inserted row, or -1 if an error occurred
     */
    public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues)
            throws SQLException {
        return insertWithOnConflict(table, nullColumnHack, initialValues, CONFLICT_REPLACE);
    }

    /**
     * General method for inserting a row into the database.
     * 
     * @param table the table to insert the row into
     * @param nullColumnHack SQL doesn't allow inserting a completely empty row, so if initialValues
     *        is empty this column will explicitly be assigned a NULL value
     * @param initialValues this map contains the initial column values for the row. The keys should
     *        be the column names and the values the column values
     * @param conflictAlgorithm for insert conflict resolver
     * @return the row ID of the newly inserted row OR the primary key of the existing row if the
     *         input param 'conflictAlgorithm' = {@link #CONFLICT_IGNORE} OR -1 if any error
     */
    public long insertWithOnConflict(String table, String nullColumnHack,
            ContentValues initialValues, int conflictAlgorithm) {
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }

        // Measurements show most sql lengths <= 152
        StringBuilder sql = new StringBuilder(152);
        sql.append("INSERT");
        sql.append(CONFLICT_VALUES[conflictAlgorithm]);
        sql.append(" INTO ");
        sql.append(table);
        // Measurements show most values lengths < 40
        StringBuilder values = new StringBuilder(40);

        Set<Map.Entry<String, Object>> entrySet = null;
        if (initialValues != null && initialValues.size() > 0) {
            entrySet = initialValues.valueSet();
            Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
            sql.append('(');

            boolean needSeparator = false;
            while (entriesIter.hasNext()) {
                if (needSeparator) {
                    sql.append(", ");
                    values.append(", ");
                }
                needSeparator = true;
                Map.Entry<String, Object> entry = entriesIter.next();
                sql.append(entry.getKey());
                values.append('?');
            }

            sql.append(')');
        } else {
            sql.append("(" + nullColumnHack + ") ");
            values.append("NULL");
        }

        sql.append(" VALUES(");
        sql.append(values);
        sql.append(");");

        lock();
        SQLiteStatement statement = null;
        try {
            statement = compileStatement(sql.toString());

            // Bind the values
            if (entrySet != null) {
                int size = entrySet.size();
                Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();
                for (int i = 0; i < size; i++) {
                    Map.Entry<String, Object> entry = entriesIter.next();
                    DatabaseUtils.bindObjectToProgram(statement, i + 1, entry.getValue());

                }
            }

            // Run the program and then cleanup
            statement.execute();

            long insertedRowId = lastInsertRow();
            if (insertedRowId == -1) {
                Log.e(TAG, "Error inserting " + initialValues + " using " + sql);
            } else {
                if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Inserting row " + insertedRowId + " from " + initialValues
                            + " using " + sql);
                }
            }
            return insertedRowId;
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            if (statement != null) {
                statement.close();
            }
            unlock();
        }
    }

    /**
     * Convenience method for deleting rows in the database.
     * 
     * @param table the table to delete from
     * @param whereClause the optional WHERE clause to apply when deleting. Passing null will delete
     *        all rows.
     * @return the number of rows affected if a whereClause is passed in, 0 otherwise. To remove all
     *         rows and get a count pass "1" as the whereClause.
     */
    public int delete(String table, String whereClause, String[] whereArgs) {
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        SQLiteStatement statement = null;
        try {
            statement =
                    compileStatement("DELETE FROM " + table
                            + (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""));
            if (whereArgs != null) {
                int numArgs = whereArgs.length;
                for (int i = 0; i < numArgs; i++) {
                    DatabaseUtils.bindObjectToProgram(statement, i + 1, whereArgs[i]);
                }
            }
            statement.execute();
            return lastChangeCount();
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            if (statement != null) {
                statement.close();
            }
            unlock();
        }
    }

    /**
     * Convenience method for updating rows in the database.
     * 
     * @param table the table to update in
     * @param values a map from column names to new column values. null is a valid value that will
     *        be translated to NULL.
     * @param whereClause the optional WHERE clause to apply when updating. Passing null will update
     *        all rows.
     * @return the number of rows affected
     */
    public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
        return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
    }

    /**
     * Convenience method for updating rows in the database.
     * 
     * @param table the table to update in
     * @param values a map from column names to new column values. null is a valid value that will
     *        be translated to NULL.
     * @param whereClause the optional WHERE clause to apply when updating. Passing null will update
     *        all rows.
     * @param conflictAlgorithm for update conflict resolver
     * @return the number of rows affected
     */
    public int updateWithOnConflict(String table, ContentValues values, String whereClause,
            String[] whereArgs, int conflictAlgorithm) {
        if (values == null || values.size() == 0) {
            throw new IllegalArgumentException("Empty values");
        }

        StringBuilder sql = new StringBuilder(120);
        sql.append("UPDATE ");
        sql.append(CONFLICT_VALUES[conflictAlgorithm]);
        sql.append(table);
        sql.append(" SET ");

        Set<Map.Entry<String, Object>> entrySet = values.valueSet();
        Iterator<Map.Entry<String, Object>> entriesIter = entrySet.iterator();

        while (entriesIter.hasNext()) {
            Map.Entry<String, Object> entry = entriesIter.next();
            sql.append(entry.getKey());
            sql.append("=?");
            if (entriesIter.hasNext()) {
                sql.append(", ");
            }
        }

        if (!TextUtils.isEmpty(whereClause)) {
            sql.append(" WHERE ");
            sql.append(whereClause);
        }

        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        SQLiteStatement statement = null;
        try {
            statement = compileStatement(sql.toString());

            // Bind the values
            int size = entrySet.size();
            entriesIter = entrySet.iterator();
            int bindArg = 1;
            for (int i = 0; i < size; i++) {
                Map.Entry<String, Object> entry = entriesIter.next();
                DatabaseUtils.bindObjectToProgram(statement, bindArg, entry.getValue());
                bindArg++;
            }

            if (whereArgs != null) {
                size = whereArgs.length;
                for (int i = 0; i < size; i++) {
                    statement.bindString(bindArg, whereArgs[i]);
                    bindArg++;
                }
            }

            // Run the program and then cleanup
            statement.execute();
            int numChangedRows = lastChangeCount();
            if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql);
            }
            return numChangedRows;
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } catch (SQLException e) {
            Log.e(TAG, "Error updating " + values + " using " + sql);
            throw e;
        } finally {
            if (statement != null) {
                statement.close();
            }
            unlock();
        }
    }

    /**
     * Execute a single SQL statement that is not a query. For example, CREATE TABLE, DELETE,
     * INSERT, etc. Multiple statements separated by ;s are not supported. it takes a write lock
     * 
     * @throws SQLException If the SQL string is invalid for some reason
     */
    public void execSQL(String sql) throws SQLException {
        long timeStart = SystemClock.uptimeMillis();
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX);
        try {
            native_execSQL(sql);
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            unlock();
        }

        // Log commit statements along with the most recently executed
        // SQL statement for disambiguation. Note that instance
        // equality to COMMIT_SQL is safe here.
        if (sql == COMMIT_SQL) {
            logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL);
        } else {
            logTimeStat(sql, timeStart, null);
        }
    }

    public void rawExecSQL(String sql) {
        long timeStart = SystemClock.uptimeMillis();
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX);
        try {
            native_rawExecSQL(sql);
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            unlock();
        }

        // Log commit statements along with the most recently executed
        // SQL statement for disambiguation. Note that instance
        // equality to COMMIT_SQL is safe here.
        if (sql == COMMIT_SQL) {
            logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL);
        } else {
            logTimeStat(sql, timeStart, null);
        }
    }

    /**
     * Execute a single SQL statement that is not a query. For example, CREATE TABLE, DELETE,
     * INSERT, etc. Multiple statements separated by ;s are not supported. it takes a write lock,
     * 
     * @param sql
     * @param bindArgs only byte[], String, Long and Double are supported in bindArgs.
     * @throws SQLException If the SQL string is invalid for some reason
     */
    public void execSQL(String sql, Object[] bindArgs) throws SQLException {
        if (bindArgs == null) {
            throw new IllegalArgumentException("Empty bindArgs");
        }
        long timeStart = SystemClock.uptimeMillis();
        lock();
        if (!isOpen()) {
            throw new IllegalStateException("database not open");
        }
        SQLiteStatement statement = null;
        try {
            statement = compileStatement(sql);
            if (bindArgs != null) {
                int numArgs = bindArgs.length;
                for (int i = 0; i < numArgs; i++) {
                    DatabaseUtils.bindObjectToProgram(statement, i + 1, bindArgs[i]);
                }
            }
            statement.execute();
        } catch (SQLiteDatabaseCorruptException e) {
            onCorruption();
            throw e;
        } finally {
            if (statement != null) {
                statement.close();
            }
            unlock();
        }
        logTimeStat(sql, timeStart);
    }

    @Override
    protected void finalize() {
        if (isOpen()) {
            Log.e(TAG, "close() was never explicitly called on database '" + mPath + "' ",
                    mStackTrace);
            closeClosable();
            onAllReferencesReleased();
        }
    }

    public SQLiteDatabase(String path, String password, CursorFactory factory, int flags) {
        this(path, password, factory, flags, null);
    }

    /**
     * Private constructor. See {@link #create} and {@link #openDatabase}.
     * 
     * @param path The full path to the database
     * @param factory The factory to use when creating cursors, may be NULL.
     * @param flags 0 or {@link #NO_LOCALIZED_COLLATORS}. If the database file already exists,
     *        mFlags will be updated appropriately.
     */
    public SQLiteDatabase(String path, String password, CursorFactory factory, int flags,
            SQLiteDatabaseHook databaseHook) {

        if (path == null) {
            throw new IllegalArgumentException("path should not be null");
        }
        mFlags = flags;
        mPath = path;
        mSlowQueryThreshold = -1;// SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1);
        mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
        mFactory = factory;
        mPrograms = new WeakHashMap<SQLiteClosable, Object>();
        dbopen(mPath, mFlags);

        if (databaseHook != null) {
            databaseHook.preKey(this);
        }

        native_key(password.toCharArray());

        if (databaseHook != null) {
            databaseHook.postKey(this);
        }

        if (SQLiteDebug.DEBUG_SQL_CACHE) {
            mTimeOpened = getTime();
        }
        try {
            setLocale(Locale.getDefault());
        } catch (RuntimeException e) {
            Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e);
            dbclose();
            if (SQLiteDebug.DEBUG_SQL_CACHE) {
                mTimeClosed = getTime();
            }
            throw e;
        }
    }

    private String getTime() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis());
    }

    /**
     * return whether the DB is opened as read only.
     * 
     * @return true if DB is opened as read only
     */
    public boolean isReadOnly() {
        return (mFlags & OPEN_READ_MASK) == OPEN_READONLY;
    }

    /**
     * @return true if the DB is currently open (has not been closed)
     */
    public boolean isOpen() {
        return mNativeHandle != 0;
    }

    public boolean needUpgrade(int newVersion) {
        return newVersion > getVersion();
    }

    /**
     * Getter for the path to the database file.
     * 
     * @return the path to our database file.
     */
    public final String getPath() {
        return mPath;
    }

    /* package */void logTimeStat(String sql, long beginMillis) {
        logTimeStat(sql, beginMillis, null);
    }

    /* package */void logTimeStat(String sql, long beginMillis, String prefix) {
        // Keep track of the last statement executed here, as this is
        // the common funnel through which all methods of hitting
        // libsqlite eventually flow.
        mLastSqlStatement = sql;

        // Sample fast queries in proportion to the time taken.
        // Quantize the % first, so the logged sampling probability
        // exactly equals the actual sampling rate for this query.

        int samplePercent;
        long durationMillis = SystemClock.uptimeMillis() - beginMillis;
        if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) {
            // The common case is locks being uncontended. Don't log those,
            // even at 1%, which is our default below.
            return;
        }
        if (sQueryLogTimeInMillis == 0) {
            sQueryLogTimeInMillis = 500;// SystemProperties.getInt("db.db_operation.threshold_ms",
                                        // 500);
        }
        if (durationMillis >= sQueryLogTimeInMillis) {
            samplePercent = 100;
        } else {
            ;
            samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1;
            if (mRandom.nextInt(100) >= samplePercent)
                return;
        }

        // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do
        // it here so we avoid allocating in the common case.
        if (prefix != null) {
            sql = prefix + sql;
        }

        if (sql.length() > QUERY_LOG_SQL_LENGTH)
            sql = sql.substring(0, QUERY_LOG_SQL_LENGTH);

        // ActivityThread.currentPackageName() only returns non-null if the
        // current thread is an application main thread. This parameter tells
        // us whether an event loop is blocked, and if so, which app it is.
        //
        // Sadly, there's no fast way to determine app name if this is *not* a
        // main thread, or when we are invoked via Binder (e.g. ContentProvider).
        // Hopefully the full path to the database will be informative enough.

        // TODO get the current package name
        String blockingPackage = "unknown";// ActivityThread.currentPackageName();
        if (blockingPackage == null)
            blockingPackage = "";

        /*
         * EventLog.writeEvent( EVENT_DB_OPERATION, getPathForLogs(), sql, durationMillis,
         * blockingPackage, samplePercent);
         */
    }

    /**
     * Removes email addresses from database filenames before they're logged to the EventLog where
     * otherwise apps could potentially read them.
     */
    private String getPathForLogs() {
        if (mPathForLogs != null) {
            return mPathForLogs;
        }
        if (mPath == null) {
            return null;
        }
        if (mPath.indexOf('@') == -1) {
            mPathForLogs = mPath;
        } else {
            mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY");
        }
        return mPathForLogs;
    }

    /**
     * Sets the locale for this database. Does nothing if this database has the
     * NO_LOCALIZED_COLLATORS flag set or was opened read only.
     * 
     * @throws SQLException if the locale could not be set. The most common reason for this is that
     *         there is no collator available for the locale you requested. In this case the
     *         database remains unchanged.
     */
    public void setLocale(Locale locale) {
        lock();
        try {
            native_setLocale(locale.toString(), mFlags);
        } finally {
            unlock();
        }
    }

    /*
     * ============================================================================
     * 
     * The following methods deal with compiled-sql cache
     * ============================================================================
     */
    /**
     * adds the given sql and its compiled-statement-id-returned-by-sqlite to the cache of
     * compiledQueries attached to 'this'.
     * 
     * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, the new
     * {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current mapping is
     * NOT replaced with the new mapping).
     */
    /* package */void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) {
        if (mMaxSqlCacheSize == 0) {
            // for this database, there is no cache of compiled sql.
            if (SQLiteDebug.DEBUG_SQL_CACHE) {
                Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql);
            }
            return;
        }

        SQLiteCompiledSql compiledSql = null;
        synchronized (mCompiledQueries) {
            // don't insert the new mapping if a mapping already exists
            compiledSql = mCompiledQueries.get(sql);
            if (compiledSql != null) {
                return;
            }
            // add this <sql, compiledStatement> to the cache
            if (mCompiledQueries.size() == mMaxSqlCacheSize) {
                /*
                 * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. log a warning
                 * MAX_WARNINGS_ON_CACHESIZE_CONDITION times chances are it is NOT using ? for
                 * bindargs - so caching is useless. TODO: either let the callers set max cchesize
                 * for their app, or intelligently figure out what should be cached for a given app.
                 */
                if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) {
                    Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database "
                            + getPath() + "; i.e., NO space for this sql statement in cache: "
                            + sql + ". Please change your sql statements to use '?' for "
                            + "bindargs, instead of using actual values");
                }
                // don't add this entry to cache
            } else {
                // cache is NOT full. add this to cache.
                mCompiledQueries.put(sql, compiledStatement);
                if (SQLiteDebug.DEBUG_SQL_CACHE) {
                    Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + mCompiledQueries.size()
                            + "|" + sql);
                }
            }
        }
        return;
    }


    private void deallocCachedSqlStatements() {
        synchronized (mCompiledQueries) {
            for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) {
                compiledSql.releaseSqlStatement();
            }
            mCompiledQueries.clear();
        }
    }

    /**
     * from the compiledQueries cache, returns the compiled-statement-id for the given sql. returns
     * null, if not found in the cache.
     */
    /* package */SQLiteCompiledSql getCompiledStatementForSql(String sql) {
        SQLiteCompiledSql compiledStatement = null;
        boolean cacheHit;
        synchronized (mCompiledQueries) {
            if (mMaxSqlCacheSize == 0) {
                // for this database, there is no cache of compiled sql.
                if (SQLiteDebug.DEBUG_SQL_CACHE) {
                    Log.v(TAG, "|cache NOT found|" + getPath());
                }
                return null;
            }
            cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null;
        }
        if (cacheHit) {
            mNumCacheHits++;
        } else {
            mNumCacheMisses++;
        }

        if (SQLiteDebug.DEBUG_SQL_CACHE) {
            Log.v(TAG, "|cache_stats|" + getPath() + "|" + mCompiledQueries.size() + "|"
                    + mNumCacheHits + "|" + mNumCacheMisses + "|" + cacheHit + "|" + mTimeOpened
                    + "|" + mTimeClosed + "|" + sql);
        }
        return compiledStatement;
    }

    /**
     * returns true if the given sql is cached in compiled-sql cache.
     * 
     * @hide
     */
    public boolean isInCompiledSqlCache(String sql) {
        synchronized (mCompiledQueries) {
            return mCompiledQueries.containsKey(sql);
        }
    }

    /**
     * purges the given sql from the compiled-sql cache.
     * 
     * @hide
     */
    public void purgeFromCompiledSqlCache(String sql) {
        synchronized (mCompiledQueries) {
            mCompiledQueries.remove(sql);
        }
    }

    /**
     * remove everything from the compiled sql cache
     * 
     * @hide
     */
    public void resetCompiledSqlCache() {
        synchronized (mCompiledQueries) {
            mCompiledQueries.clear();
        }
    }

    /**
     * return the current maxCacheSqlCacheSize
     * 
     * @hide
     */
    public synchronized int getMaxSqlCacheSize() {
        return mMaxSqlCacheSize;
    }

    /**
     * set the max size of the compiled sql cache for this database after purging the cache. (size
     * of the cache = number of compiled-sql-statements stored in the cache).
     * 
     * max cache size can ONLY be increased from its current size (default = 0). if this method is
     * called with smaller size than the current value of mMaxSqlCacheSize, then
     * IllegalStateException is thrown
     * 
     * synchronized because we don't want t threads to change cache size at the same time.
     * 
     * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE)
     * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or < the value
     *         set with previous setMaxSqlCacheSize() call.
     * 
     * @hide
     */
    public synchronized void setMaxSqlCacheSize(int cacheSize) {
        if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
            throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE);
        } else if (cacheSize < mMaxSqlCacheSize) {
            throw new IllegalStateException("cannot set cacheSize to a value less than the value "
                    + "set with previous setMaxSqlCacheSize() call.");
        }
        mMaxSqlCacheSize = cacheSize;
    }

    static class ActiveDatabases {
        private static final ActiveDatabases activeDatabases = new ActiveDatabases();
        private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases =
                new HashSet<WeakReference<SQLiteDatabase>>();

        private ActiveDatabases() {} // disable instantiation of this class

        static ActiveDatabases getInstance() {
            return activeDatabases;
        }
    }

    /**
     * this method is used to collect data about ALL open databases in the current process.
     * bugreport is a user of this data.
     */
    /* package */static ArrayList<DbStats> getDbStats() {
        ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
        for (WeakReference<SQLiteDatabase> w : ActiveDatabases.getInstance().mActiveDatabases) {
            SQLiteDatabase db = w.get();
            if (db == null || !db.isOpen()) {
                continue;
            }
            // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db
            int lookasideUsed = db.native_getDbLookaside();

            // get the lastnode of the dbname
            String path = db.getPath();
            int indx = path.lastIndexOf("/");
            String lastnode = path.substring((indx != -1) ? ++indx : 0);

            // get list of attached dbs and for each db, get its size and pagesize
            ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db);
            if (attachedDbs == null) {
                continue;
            }
            for (int i = 0; i < attachedDbs.size(); i++) {
                Pair<String, String> p = attachedDbs.get(i);
                long pageCount = getPragmaVal(db, p.first + ".page_count;");

                // first entry in the attached db list is always the main database
                // don't worry about prefixing the dbname with "main"
                String dbName;
                if (i == 0) {
                    dbName = lastnode;
                } else {
                    // lookaside is only relevant for the main db
                    lookasideUsed = 0;
                    dbName = "  (attached) " + p.first;
                    // if the attached db has a path, attach the lastnode from the path to above
                    if (p.second.trim().length() > 0) {
                        int idx = p.second.lastIndexOf("/");
                        dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0);
                    }
                }
                if (pageCount > 0) {
                    dbStatsList
                            .add(new DbStats(dbName, pageCount, db.getPageSize(), lookasideUsed));
                }
            }
        }
        return dbStatsList;
    }

    /**
     * get the specified pragma value from sqlite for the specified database. only handles pragma's
     * that return int/long. NO JAVA locks are held in this method. TODO: use this to do all
     * pragma's in this class
     */
    private static long getPragmaVal(SQLiteDatabase db, String pragma) {
        if (!db.isOpen()) {
            return 0;
        }
        SQLiteStatement prog = null;
        try {
            prog = new SQLiteStatement(db, "PRAGMA " + pragma);
            long val = prog.simpleQueryForLong();
            return val;
        } finally {
            if (prog != null)
                prog.close();
        }
    }

    /**
     * returns list of full pathnames of all attached databases including the main database TODO:
     * move this to {@link DatabaseUtils}
     */
    private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) {
        if (!dbObj.isOpen()) {
            return null;
        }
        ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
        Cursor c = dbObj.rawQuery("pragma database_list;", null);
        while (c.moveToNext()) {
            attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
        }
        c.close();
        return attachedDbs;
    }

    /**
     * Sets the root directory to search for the ICU data file
     */
    public static native void setICURoot(String path);

    /**
     * Native call to open the database.
     * 
     * @param path The full path to the database
     */
    private native void dbopen(String path, int flags);

    /**
     * Native call to setup tracing of all sql statements
     * 
     * @param path the full path to the database
     */
    private native void enableSqlTracing(String path);

    /**
     * Native call to setup profiling of all sql statements. currently, sqlite's profiling =
     * printing of execution-time (wall-clock time) of each of the sql statements, as they are
     * executed.
     * 
     * @param path the full path to the database
     */
    private native void enableSqlProfiling(String path);

    /**
     * Native call to execute a raw SQL statement. {@link #lock} must be held when calling this
     * method.
     * 
     * @param sql The raw SQL string
     * @throws SQLException
     */
    /* package */native void native_execSQL(String sql) throws SQLException;

    /**
     * Native call to set the locale. {@link #lock} must be held when calling this method.
     * 
     * @throws SQLException
     */
    /* package */native void native_setLocale(String loc, int flags);

    /**
     * Returns the row ID of the last row inserted into the database.
     * 
     * @return the row ID of the last row inserted into the database.
     */
    /* package */native long lastInsertRow();

    /**
     * Returns the number of changes made in the last statement executed.
     * 
     * @return the number of changes made in the last statement executed.
     */
    /* package */native int lastChangeCount();

    /**
     * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here
     * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html
     * 
     * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED
     */
    private native int native_getDbLookaside();

    private native void native_rawExecSQL(String sql);

    private native int native_status(int operation, boolean reset);

    private native void native_key(char[] key) throws SQLException;

    private native void native_key(String key) throws SQLException;
}
